title: async 函数
date: 2018.9.9
tags:


2018.9.9 星期日 22:53

1. 含义
2. 基本用法
3. 语法
4. async 函数的实现原理
5. 与其他异步处理方法的比较
6. 实例:按顺序完成异步操作
7. 异步遍历器

1 含义

它就是 Generator 函数的语法糖。
体现在以下四点。
(1)内置执行器。
(2)更好的语义。
(3)更广的适用性。
(4)返回值是 Promise:Generator 函数的返回值是 Iterator 对象方便多了

进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

2 基本用法

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。
当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

function timeout(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

async function asyncPrint(value, ms) {
  await timeout(ms);
  console.log(value);
}

asyncPrint('hello world', 50);// 50 毫秒以后,输出hello world。

由于async函数返回的是 Promise 对象,可以作为await命令的参数

async 函数有多种使用形式:
// 函数声明
// 函数表达式
// 对象的方法
// Class 的方法
// 箭头函数

3 语法

async函数的语法规则总体上比较简单,难点是错误处理机制。

返回 Promise 对象

async函数返回一个 Promise 对象。
async函数内部return语句返回的值,会成为then方法回调函数的参数。

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

Promise 对象的状态变化

也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

await 命令

正常情况下,await命令后面是一个 Promise 对象。
如果不是,会被转成一个立即resolve的 Promise 对象。

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。
只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时..

错误处理

防止出错的方法,也是将其放在try…catch代码块之中。

使用注意点

第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try…catch代码块中。
第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。
第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

let foo = await getFoo();
let bar = await getBar();
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

// #### 如果确实希望多个请求并发执行,除了Promise.all
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = await Promise.all(promises);
  console.log(results);
}
// 或者使用下面的写法
async function dbFuc(db) {
  let docs = [{}, {}, {}];
  let promises = docs.map((doc) => db.post(doc));

  let results = [];
  for (let promise of promises) {
    results.push(await promise);
  }
  console.log(results);
}

下面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。
如果确实希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。

function dbFuc(db) { //这里不需要 async
  let docs = [{}, {}, {}];
  // 可能得到错误结果
  docs.forEach(async function (doc) {
    await db.post(doc);
  });
}

目前,esm模块加载器支持顶层await,即await命令可以不放在 async 函数里面,直接使用。
第二种写法的脚本必须使用esm加载器,才会生效。

// async 函数的写法
const start = async () => {
  const res = await fetch('google.com');
  return res.text();
};

start().then(console.log);

// 顶层 await 的写法
const res = await fetch('google.com');
console.log(await res.text());

4 async 函数的实现原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

5 与其他异步处理方法的比较

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

首先是 Promise 的写法。
接着是 Generator 函数的写法。
最后是 async 函数的写法。

6 实例:按顺序完成异步操作

实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

Promise 的写法如下。
这种写法不太直观,可读性比较差。下面是 async 函数实现。
上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
  }
}
// 并发发出远程请求。
async function logInOrder(urls) {
  // 并发读取远程URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url);
    return response.text();
  });

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise);
  }
}

上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

23:37

7 异步遍历器

《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法返回的对象的结构是{value, done},其中value表示当前的数据的值,done是一个布尔值,表示遍历是否结束。
这里隐含着一个规定,next方法必须是同步的,只要调用就必须立刻返回值。也就是

ES2018 引入了”异步遍历器“(Async Iterator),为异步操作提供原生的遍历器接口,即value和done这两个属性都是异步产生。

### 异步遍历的接口
异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

for await…of

异步 Generator 函数

yield* 语句